#!/usr/bin/env python

#   Trolls Outta Luckland v0.0.6
#   Copyright 2009-2010, Gummbum
#
#   This file is part of Trolls Outta Luckland.
#
#   Trolls Outta Luckland is free software: you can redistribute it
#   and/or modify it under the terms of the GNU General Public License
#   as published by the Free Software Foundation, either version 3 of
#   the License, or (at your option) any later version.
#
#   Trolls Outta Luckland is distributed in the hope that it will be
#   useful, but WITHOUT ANY WARRANTY; without even the implied warranty
#   of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with Trolls Outta Luckland.  If not, see
#   <http://www.gnu.org/licenses/>.

__version__ = '0.0.3'
__revision__ = '0.0.6'

## Python imports
import binascii
from cStringIO import StringIO
import os
import sys
import random
from random import randrange as rand

try:
    ## Pygame imports
    import pygame
    ## Game library imports
    import game_globals
    from game_globals import *
except ImportError, e:
    print 'ImportError: %s' % e
    print '    in file "%s"' % os.path.abspath(sys.argv[0])

## Holds the system-wide jukebox, a dictionay of sounds keyed by name. Can
## be accessed after constructing the first Jukebox object. Subsequent
## constructions of Jukebox create only objects local to the calling scope.
jukebox = None

# Each sound combines four volume settings:
#   master: this affects everything
#   type: this affects sounds tagged with a particular type
#   unit: this affects the sole sound; its effect is persistent
#   play: this is an option of _Sound.play(); its effect is temporary
# The playback volume is master*type*unit. If they are all 1, the playback
# volume will be one. Any other combinations will yield playback<1.
## Before loading any music you will want to adjust your levels:
# 1. Use jukebox.set_master_volume(). If you do not set this, sounds will play
#    using the MASTER_VOLUME setting.
# 2. Use jukebox.set_default_volume(). If you do not set this, sounds will be
#    initialized with the DEFAULT_VOLUME setting when they are loaded. You can
#    also set this per unit via the jukebox.load_*() volume option.
# 3. Use jukebox.set_type_volume() for each sound type you intend to use. If
#    you don't use any types, then sounds are all assigned to the 'default'
#    type which has a default volume of 1. If you use types and do not
#    initialize the volume level it will default to 1.
#
# However, with the exception of DEFAULT_VOLUME you can easily change the
# levels via jukebox methods before or after you start playing the sounds.
# Changing DEFAULT_VOLUME after sounds are loaded does not affect the
# existing sounds.
#
# It's easiest to set the unit sound while loading each unit.
MASTER_VOLUME = 0.6
DEFAULT_VOLUME = 1
TYPE_VOLUMES = {'default':1}

class Jukebox(dict):
    """dictionary of sounds keyed by name
    
    The first time this class is constructed, the object is saved as the
    jukebox.jukebox module variable. Subsequent constructions become
    dictionaries local to the caller. Multiple jukebox objects can coexist,
    but only one jukebox's settings can be in effect in the pygame.mixer at
    a time.
    
    Mixer settings can be changed on jukebox objects by setting the
    attributes at any time. The settings will affect the mixer only when
    jukeboxobject.init() is called. Changing mixer settings will interrupt
    any playing sounds.
    """
    def __init__(self, frequency=44100, size=-16, channels=16, buffer=1024):
        global jukebox
        self.frequency = frequency
        self.size = size
        self.channels = channels
        self.buffer = buffer
        if jukebox is None:
            self.init()
            jukebox = self
    def init(self):
        """restart the mixer with the currently stored settings"""
        pygame.mixer.quit()
        pygame.mixer.init(
            self.frequency, self.size, self.channels, self.buffer)
    def load_ascii_file(self,filename, name=None, volume=DEFAULT_VOLUME,
    sound_type='default', reload=False):
        """load a sound from a file in Base64 format
        
        See comments at jukebox._Base64SoundFile.
        """
        global TYPE_VOLUMES
        if sound_type not in TYPE_VOLUMES:
            TYPE_VOLUMES[sound_type] = 1
        if name in self and reload is False:
            return self[name]
        self[name] = _Base64SoundFile(filename, volume, name, sound_type)
        return self[name]
    def load_ascii_array(self,
        base64_array, name, volume=DEFAULT_VOLUME, sound_type='default', reload=False):
        """load a sound from an array in Base64 format
        
        See comments at jukebox._Base64SoundArray.
        """
        global TYPE_VOLUMES
        if sound_type not in TYPE_VOLUMES:
            TYPE_VOLUMES[sound_type] = 1
        if name in self and reload is False:
            return self[name]
        self[name] = _Base64SoundArray(base64_array, volume, name, sound_type)
        return self[name]
    def load_sound(self,
        source, name=None, volume=DEFAULT_VOLUME, sound_type='default', reload=False):
        """load a sound file in a format supported by Pygame"""
        global TYPE_VOLUMES
        if name is None:
            name = source
        if name in self and reload is False:
            return self[name]
        self[name] = _Sound(source, volume, name, sound_type)
        if sound_type not in TYPE_VOLUMES:
            TYPE_VOLUMES[sound_type] = 1
        return self[name]
    def get_master_volume(self):
        return MASTER_VOLUME
    def get_default_volume(self):
        return DEFAULT_VOLUME
    def get_type_volume(self, sound_type):
        if sound_type in TYPE_VOLUMES:
            return TYPE_VOLUMES[sound_type]
        else:
            return None
    def set_default_volume(self, volume):
        global DEFAULT_VOLUME
        if volume > 1:
            volume = 1
        elif volume < 0:
            volume = 0
        DEFAULT_VOLUME = volume
    def adjust_master_volume(self, adjust):
        return self.set_master_volume(MASTER_VOLUME + adjust)
    def set_master_volume(self, volume):
        global MASTER_VOLUME
        if volume > 1:
            volume = 1
        elif volume < 0:
            volume = 0
        MASTER_VOLUME = volume
        for s in self.values():
            s.set_volume(s.get_volume())
        return MASTER_VOLUME
    def adjust_type_volume(self, adjust, sound_type=None):
        volume = TYPE_VOLUMES[sound_type] + adjust
        return self.set_type_volume(volume, sound_type)
    def set_type_volume(self, volume, sound_type=None):
        if volume > 1:
            volume = 1
        elif volume < 0:
            volume = 0
        TYPE_VOLUMES[sound_type] = volume
        for s in self.values():
            if s.type is sound_type:
                s.set_type_volume(volume)
        return volume

class _Sound(pygame.mixer.Sound):
    """construct a pygame.mixer.Sound from a music file
    
    This subclass adds or overrides the following methods:
        __init__(source, volume) - adds volume parameter
        play_mutex() - play a sound only if it is not already playing
        fadeout(milli=1000) - set the default for milli
    """
    def __init__(self, source, unit_volume, name='unknown', sound_type='default'):
        self.type = sound_type
        self.name = name
        self._mutex_channel = None
        self._unit_volume = unit_volume
        pygame.mixer.Sound.__init__(self, source)
    def fadeout(self, milli=1000):
        pygame.mixer.Sound.fadeout(self, milli)
    def play_mutex(self, volume=1):
        if self._mutex_channel is None or \
        self._mutex_channel.get_sound() is not self:
            return self.play(volume)
        else:
            return self._mutex_channel
    def set_unit_volume(self, volume):
        if volume > 1:
            volume = 1
        elif volume < 0:
            volume = 0
        self._unit_volume = volume
        self.set_volume(self.get_volume())
    def set_type_volume(self, volume):
        if volume > 1:
            volume = 1
        elif volume < 0:
            volume = 0
        self._type_volume = volume
        self.set_volume(self.get_volume())
    def get_volume(self):
        return self._unit_volume * TYPE_VOLUMES[self.type] * MASTER_VOLUME
    def play(self, volume=1):
        volume *= self.get_volume()
        self.set_volume(volume)
        self._mutex_channel = pygame.mixer.Sound.play(self)
        return self._mutex_channel

class _Base64SoundArray(_Sound):
    """construct a jukebox._Sound from a Base64-encoded array
    
    The array must have been fashioned by a method similar to
    jukebox.Base64SoundFile.
    """
    def __init__(self, base64_array, volume, name='unknown', sound_type='default'):
        self.name = name
        sound_string = binascii.a2b_base64(base64_array)
        sound_array = StringIO(sound_string)
        _Sound.__init__(self, sound_array, volume, name, sound_type)

class _Base64SoundFile(_Base64SoundArray):
    """construct a jukebox._Base64SoundArray from a Base64-encoded file
    
    The file must have been fashioned by a method equivalent to
    jukebox.bin2asc.
    """
    def __init__(self, filename, volume, name=None, sound_type='default'):
        if name is None:
            name = filename
        f = open(filename, 'rb')
        base64_array = f.read()
        f.close()
        _Base64SoundArray.__init__(self, base64_array, volume, name, sound_type)

def bin2asc(source_filename, dest_filename):
    """convert a binary file (e.g. OGG, WAV) to Base64"""
    f = open(source_filename, 'rb')
    bin_data = f.read()
    asc_data = binascii.b2a_base64(bin_data)
    f.close()
    f = open(dest_filename, 'w')
    f.write(asc_data)
    f.close()

if __name__ == '__main__':
    print 'creating jukebox'
    Jukebox()
    print 'loading file "lasershortdistorted.dat" as name "laser"'
    jukebox.load_ascii_file('sound/lasershortdistorted.dat', 'laser')
    print 'playing sound "laser"'
    channel = jukebox['laser'].play()
    while channel.get_busy() and \
    channel.get_sound() is jukebox['laser']:
        pygame.time.wait(1000)
    #
    jukebox.load_sound('music/DJMesoPhunk_-_Dream_of_Flight_(In_the_Sky_Mix)_-_Mnemosyne_feat_Queenie.ogg', 'song')
    channel = jukebox['song'].play()
    while channel.get_busy() and \
    channel.get_sound() is jukebox['song']:
        pygame.time.wait(1000)
